Skip to content

Java 核心类库与字符串处理

Java 核心类库与字符串处理


1. 概述

Java 核心类库(java.langjava.utiljava.time)是所有 Java 程序的基础支撑。Object 根类定义了对象的通用契约,String 是使用最频繁的不可变类,包装类连接了基本类型与泛型体系,java.time 提供了现代化的日期时间 API。熟练掌握这些类的特性、底层实现与常见陷阱,直接影响代码的正确性、性能与可维护性,也是面试中的高频考点。

本笔记覆盖范围: Object 核心方法 → String 与字符串处理 → 包装类与数字处理 → java.time 日期时间 API


2. 核心概念总览

mermaid
mindmap
  root((核心类库))
    Object
      equals / hashCode 契约
      toString
      clone(浅拷贝/深拷贝)
      finalize(已废弃)
    String
      不可变性
      字符串常量池
      StringBuilder / StringBuffer
      intern() 方法
      正则 API
    包装类
      自动装箱拆箱
      缓存池(IntegerCache)
      BigDecimal 精确计算
    java.time
      LocalDate / LocalTime / LocalDateTime
      ZonedDateTime / Instant
      DateTimeFormatter(线程安全)
      时区与夏令时

3. Object 类核心方法

java.lang.Object 是 Java 类继承体系的根类,所有类都隐式继承它。其核心方法构成了 Java 对象的基础契约

3.1 equals 与 hashCode 契约

3.1.1 契约规则

规则说明
自反性$x.equals(x)$ 必须返回 true
对称性$x.equals(y) = true$ ⟺ $y.equals(x) = true$
传递性$x.equals(y)$ 且 $y.equals(z)$,则 $x.equals(z)$
一致性对象未修改时,多次调用结果一致
非空性$x.equals(null)$ 必须返回 false
hashCode 一致性equals 相等 → hashCode 必须相同
hashCode 碰撞hashCode 相同 → equals 不一定相等

⚠️ 违反契约的后果: 对象放入 HashMap/HashSet 后可能找不到,产生难以排查的 Bug。

3.1.2 equals 重写的完整规范

java
public class User {
    private String name;
    private int age;
    private String[] tags;

    @Override
    public boolean equals(Object o) {
        // ① 引用相等,直接返回 true(性能优化)
        if (this == o) return true;
        // ② instanceof 判断类型兼容性(同时处理了 null 的情况)
        if (!(o instanceof User)) return false;
        // ③ 强转后逐字段比较
        User user = (User) o;
        return age == user.age                            // 基本类型用 ==
            && Objects.equals(name, user.name)            // 对象用 Objects.equals() 避免 NPE
            && Arrays.equals(tags, user.tags);            // 数组用 Arrays.equals()
    }

    @Override
    public int hashCode() {
        int result = Objects.hash(name, age);
        result = 31 * result + Arrays.hashCode(tags);
        return result;
    }
}

Java 14+ Record 类自动基于所有组件字段生成正确的 equals()hashCode()toString()

java
public record User(String name, int age) {}
// 无需手动重写任何方法

3.1.3 hashCode 高质量实现

核心目标: 分布均匀,减少哈希碰撞。

java
// 推荐方式一:Objects.hash()(简洁)
@Override
public int hashCode() {
    return Objects.hash(name, age, email);
}

// 推荐方式二:手动计算(性能更优,避免自动装箱)
@Override
public int hashCode() {
    int result = 17;
    result = 31 * result + (name != null ? name.hashCode() : 0);
    result = 31 * result + age;           // 基本类型直接参与计算
    result = 31 * result + (email != null ? email.hashCode() : 0);
    return result;
}

为什么选 31 作为乘数?

  • 31 是奇素数,碰撞概率低
  • JIT 编译器可将 31 * i 优化为 (i << 5) - i(移位 + 减法),性能极高

String 的 hashCode 缓存机制:

java
// JDK 源码(简化)
public final class String {
    private int hash; // 默认 0,首次计算后缓存

    public int hashCode() {
        int h = hash;
        if (h == 0 && value.length > 0) {
            for (char v : value) {
                h = 31 * h + v;
            }
            hash = h; // 缓存,因为 String 不可变所以安全
        }
        return h;
    }
}

3.1.4 hashCode 在 HashMap 中的工作原理

mermaid
flowchart LR
    A["key.hashCode()"] --> B["二次哈希<br/>h ^ (h >>> 16)"]
    B --> C["计算桶索引<br/>index = hash & (n-1)"]
    C --> D{"桶是否为空?"}
    D -- 是 --> E["直接放入"]
    D -- 否 --> F{"equals 相等?"}
    F -- 是 --> G["覆盖 value"]
    F -- 否 --> H{"链表长度 ≥ 8?"}
    H -- 否 --> I["追加到链表尾"]
    H -- 是 --> J["转为红黑树<br/>O(log n) 查找"]

关键参数:

参数说明
默认容量16桶数组初始大小
加载因子0.75达到 $容量 \times 0.75$ 时扩容
树化阈值8链表长度 ≥ 8 且容量 ≥ 64 时转红黑树
退化阈值6红黑树节点 ≤ 6 时退化回链表

二次哈希的目的: 将高 16 位的信息混入低位,使得即使用户的 hashCode() 实现质量不高,也能获得相对均匀的桶分布。

3.2 toString、clone 与 finalize

方法默认行为正确使用方式
toString()返回 类名@十六进制hashCode重写为有意义的字符串(Lombok @ToString
clone()浅拷贝(需实现 Cloneable深拷贝需手动递归 or 序列化
finalize()GC 前执行,Java 9 已废弃try-with-resources 替代

浅拷贝 vs 深拷贝:

java
public class Order implements Cloneable {
    private String orderId;
    private List<Item> items;  // 引用类型

    // 浅拷贝:items 引用指向同一个 List 对象
    @Override
    protected Order clone() throws CloneNotSupportedException {
        return (Order) super.clone();
    }

    // 深拷贝:手动复制引用类型字段
    public Order deepClone() throws CloneNotSupportedException {
        Order cloned = (Order) super.clone();
        cloned.items = new ArrayList<>();
        for (Item item : this.items) {
            cloned.items.add(item.clone()); // Item 也需实现 clone
        }
        return cloned;
    }
}

实战建议: 避免使用 clone(),优先使用拷贝构造函数序列化(如 JSON 序列化/反序列化)实现对象复制。


4. String 类与字符串处理

4.1 String 不可变性与底层存储

java
// JDK 8 及之前
public final class String {
    private final char[] value;  // UTF-16,每个字符 2 字节
}

// JDK 9+(Compact Strings)
public final class String {
    private final byte[] value;  // Latin-1 字符用 1 字节,其他用 2 字节
    private final byte coder;    // LATIN1 = 0, UTF16 = 1
}

不可变性的好处:

  1. 线程安全:无需同步即可在多线程间共享
  2. hashCode 缓存:计算一次后永久有效
  3. 安全性:作为 HashMap 的 key、类加载路径、网络连接参数等不会被篡改
  4. 字符串常量池复用:相同字面量只存一份

4.2 字符串常量池(String Pool)

java
String a = "hello";           // 字面量 → 放入常量池
String b = "hello";           // 复用常量池中的同一对象
System.out.println(a == b);   // true(同一引用)

String c = new String("hello");  // 在堆上创建新对象
System.out.println(a == c);      // false(不同引用)
System.out.println(a.equals(c)); // true(内容相等)
mermaid
graph TB
    subgraph "方法区 / 堆(JDK 7+)"
        Pool["字符串常量池<br/>'hello' 对象"]
    end
    subgraph "堆"
        HeapObj["new String('hello')<br/>堆上新对象"]
    end
    a["引用 a"] --> Pool
    b["引用 b"] --> Pool
    c["引用 c"] --> HeapObj
    HeapObj -.->|"内部 value 指向相同 char[]"| Pool

4.3 intern() 方法与常量池位置变迁

java
String s1 = new String("abc");     // 堆对象
String s2 = s1.intern();           // 将 "abc" 放入常量池(如已存在则返回池中引用)
String s3 = "abc";                 // 字面量,指向常量池
System.out.println(s2 == s3);      // true
System.out.println(s1 == s3);      // false
JDK 版本常量池位置特点
JDK 6PermGen(永久代)大小固定(-XX:MaxPermSize),大量 intern() 易 OOM
JDK 7-Xmx 控制,OOM 压力降低
JDK 8+堆中(元空间替代 PermGen)-XX:StringTableSize 调整哈希桶数

适用场景: 大量重复字符串(如 XML 标签名、CSV 字段名)使用 intern() 可显著降低内存。但滥用会导致常量池哈希表过大,反而影响性能。

4.4 字符串拼接的底层实现

java
// 场景一:编译期常量折叠
String s = "a" + "b" + "c";  // 编译后等价于 String s = "abc";

// 场景二:变量拼接(JDK 8)
String name = "World";
String greeting = "Hello, " + name + "!";
// 编译后等价于:
// new StringBuilder().append("Hello, ").append(name).append("!").toString();

// 场景三:循环内拼接(❌ 性能陷阱)
String result = "";
for (int i = 0; i < 10000; i++) {
    result += i;  // 每次循环创建新的 StringBuilder + toString() → O(n²)
}

// 正确方式 ✅
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10000; i++) {
    sb.append(i);  // 复用同一个 StringBuilder → O(n)
}
String result = sb.toString();

JDK 9+ StringConcatFactory 优化:

  • 使用 invokedynamic 指令,在运行时由 JVM 选择最优拼接策略
  • 简单场景性能接近手写 StringBuilder
  • 循环内拼接仍需手动使用 StringBuilder

4.5 StringBuilder vs StringBuffer

特性StringBuilderStringBuffer
线程安全❌ 否✅ 是(synchronized
性能慢(同步开销)
引入版本JDK 5JDK 1.0
推荐场景单线程(绝大多数场景)多线程共享(极罕见)
初始容量1616
扩容策略(oldCap << 1) + 2同 StringBuilder

实战经验: 多线程场景下字符串构建通常通过局部变量天然保证线程安全,StringBuffer 几乎没有使用场景。

4.6 String 常用 API 速查

java
String s = "  Hello, World!  ";

// 基础操作
s.length();                    // 19
s.charAt(7);                   // 'W'
s.isEmpty();                   // false(JDK 6+)
s.isBlank();                   // false(JDK 11+,检测是否全为空白字符)

// 查找
s.indexOf("World");            // 9
s.lastIndexOf('l');            // 14
s.contains("Hello");           // true
s.startsWith("  He");          // true
s.endsWith("!  ");             // true

// 截取与替换
s.substring(2, 7);             // "Hello"
s.replace('l', 'L');           // "  HeLLo, WorLd!  "
s.replaceAll("\\s+", "");      // "Hello,World!"(正则)

// 分割(注意特殊字符需转义)
"a.b.c".split("\\.");          // ["a", "b", "c"]("." 在正则中是通配符)
"a,,b".split(",", -1);         // ["a", "", "b"](-1 保留尾部空串)

// 去空白
s.trim();                      // "Hello, World!"(去除 ASCII ≤ 32 的字符)
s.strip();                     // "Hello, World!"(JDK 11+,去除 Unicode 空白)
s.stripLeading();              // "Hello, World!  "
s.stripTrailing();             // "  Hello, World!"

// 格式化
String.format("Name: %s, Age: %d", "Tom", 25);  // "Name: Tom, Age: 25"
"Name: %s".formatted("Tom");                     // JDK 15+

// 转换
s.toCharArray();               // char[]
s.getBytes(StandardCharsets.UTF_8);  // byte[]
String.join(", ", "a", "b", "c");    // "a, b, c"

// 多行文本块(JDK 15+)
String json = """
        {
            "name": "Tom",
            "age": 25
        }
        """;

4.7 正则表达式最佳实践

java
// ❌ 反复编译 Pattern(String.matches() 内部每次调用都会 Pattern.compile)
for (String line : lines) {
    if (line.matches("\\d{4}-\\d{2}-\\d{2}")) { ... }
}

// ✅ 预编译 Pattern,复用(性能提升 5~10 倍)
private static final Pattern DATE_PATTERN = Pattern.compile("\\d{4}-\\d{2}-\\d{2}");

for (String line : lines) {
    if (DATE_PATTERN.matcher(line).matches()) { ... }
}

5. 包装类与数字处理

5.1 自动装箱拆箱与缓存池

java
// 自动装箱(基本类型 → 包装类)
Integer a = 100;      // 编译器转换为 Integer.valueOf(100)

// 自动拆箱(包装类 → 基本类型)
int b = a;            // 编译器转换为 a.intValue()

// ⚠️ 拆箱 NPE 陷阱
Integer c = null;
int d = c;            // NullPointerException!

IntegerCache 缓存池(-128 ~ 127):

java
Integer x = 127;
Integer y = 127;
System.out.println(x == y);   // true(从缓存池取同一对象)

Integer m = 128;
Integer n = 128;
System.out.println(m == n);   // false(超出缓存范围,new 了不同对象)
System.out.println(m.equals(n)); // true ✅ 正确方式
包装类缓存范围
Byte-128 ~ 127(全部)
Short-128 ~ 127
Integer-128 ~ 127(可通过 -XX:AutoBoxCacheMax 调整上限)
Long-128 ~ 127
Character0 ~ 127
BooleanTRUE / FALSE(仅两个实例)
Float / Double无缓存

⚠️ 黄金法则:包装类比较永远使用 equals(),不要用 ==

5.2 包装类常用方法

java
// 类型转换
int a = Integer.parseInt("123");           // String → int
long b = Long.parseLong("999999999999");   // String → long
String s = String.valueOf(123);            // int → String
String s2 = Integer.toString(123);         // int → String

// 进制转换
Integer.toBinaryString(10);   // "1010"
Integer.toHexString(255);     // "ff"
Integer.toOctalString(8);     // "10"

// 安全比较(防止溢出)
Integer.compare(x, y);        // 返回 -1, 0, 1(不要用 x - y,可能溢出)

// 常量
Integer.MAX_VALUE;             // 2147483647(约 2.1×10⁹)
Integer.MIN_VALUE;             // -2147483648
Long.MAX_VALUE;                // 9223372036854775807(约 9.2×10¹⁸)

// Stream 方法引用(Java 8+)
stream.reduce(0, Integer::sum);
stream.reduce(Integer::max);

5.3 BigDecimal 精确计算

java
// ❌ 经典错误:double 构造
new BigDecimal(0.1);  // 实际值:0.1000000000000000055511151231257827021181583404541015625

// ✅ 正确方式:String 构造 或 valueOf
new BigDecimal("0.1");       // 精确的 0.1
BigDecimal.valueOf(0.1);     // 内部先转 String 再构造

// 四则运算
BigDecimal a = new BigDecimal("10.25");
BigDecimal b = new BigDecimal("3");

a.add(b);                                          // 13.25
a.subtract(b);                                     // 7.25
a.multiply(b);                                     // 30.75
a.divide(b, 2, RoundingMode.HALF_UP);              // 3.42(必须指定精度和舍入模式)

// ❌ 除不尽不指定舍入模式 → ArithmeticException
a.divide(b);  // 抛异常!

// 比较大小
a.compareTo(b);    // > 0(a > b)
// ⚠️ equals 的陷阱
new BigDecimal("2.0").equals(new BigDecimal("2.00"));  // false!(scale 不同)
new BigDecimal("2.0").compareTo(new BigDecimal("2.00")) == 0;  // true ✅

5.3.1 RoundingMode 舍入模式

模式说明示例(2.5 → ?)
HALF_UP四舍五入(最常用)3
HALF_DOWN五舍六入2
HALF_EVEN银行家舍入(向最近偶数舍入)2
UP远离零方向进位3
DOWN向零方向截断2
CEILING向正无穷方向3
FLOOR向负无穷方向2

实战建议: 金融系统应与业务方确认舍入规则,统一配置为常量,避免在代码各处随意指定。


6. 日期与时间(java.time)

6.1 旧 API 的痛点 vs 新 API 的优势

痛点旧 API(Date/Calendar)新 API(java.time)
可变性可变(线程不安全)不可变(线程安全)
月份0~11(0 = 一月,bug 根源)1~12(符合直觉)
日期时间区分Date 同时表示日期和时间LocalDateLocalTimeLocalDateTime 职责清晰
格式化SimpleDateFormat线程不安全DateTimeFormatter线程安全
时区TimeZone(API 繁琐)ZoneId(清晰直观)

6.2 核心类体系

mermaid
graph TB
    subgraph "无时区"
        LD["LocalDate<br/>2025-01-15"]
        LT["LocalTime<br/>14:30:00"]
        LDT["LocalDateTime<br/>2025-01-15T14:30:00"]
    end
    subgraph "有时区"
        ZDT["ZonedDateTime<br/>2025-01-15T14:30:00+08:00[Asia/Shanghai]"]
        ODT["OffsetDateTime<br/>2025-01-15T14:30:00+08:00"]
    end
    subgraph "时间戳"
        INS["Instant<br/>UTC 时间点(纳秒精度)"]
    end
    subgraph "时间量"
        DUR["Duration<br/>时间段(小时/分/秒)"]
        PER["Period<br/>日期段(年/月/日)"]
    end

    LD --> LDT
    LT --> LDT
    LDT -->|"atZone(ZoneId)"| ZDT
    ZDT -->|"toInstant()"| INS
    INS -->|"atZone(ZoneId)"| ZDT

6.3 常用操作示例

java
// ===== 创建 =====
LocalDate today = LocalDate.now();                          // 2025-07-11
LocalDate birthday = LocalDate.of(1995, 3, 15);            // 1995-03-15
LocalTime now = LocalTime.now();                            // 14:30:45.123
LocalDateTime dateTime = LocalDateTime.of(today, now);
ZonedDateTime shanghai = ZonedDateTime.now(ZoneId.of("Asia/Shanghai"));
Instant timestamp = Instant.now();                          // UTC 时间戳

// ===== 操作(返回新对象) =====
LocalDate nextWeek = today.plusWeeks(1);
LocalDate lastMonth = today.minusMonths(1);
LocalDate adjusted = today.withDayOfMonth(1);               // 本月第一天
LocalDate lastDay = today.withDayOfMonth(today.lengthOfMonth()); // 本月最后一天

// ===== 计算间隔 =====
Period period = Period.between(birthday, today);            // 30年4月...
Duration duration = Duration.between(startTime, endTime);   // PT2H30M
long days = ChronoUnit.DAYS.between(birthday, today);      // 总天数

// ===== 判断 =====
today.isBefore(birthday);    // false
today.isAfter(birthday);     // true
Year.of(2024).isLeap();      // true(闰年)

6.4 DateTimeFormatter 格式化

java
// ✅ 线程安全,可声明为 static final 常量
private static final DateTimeFormatter FORMATTER =
    DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

// 格式化
String str = LocalDateTime.now().format(FORMATTER);    // "2025-07-11 14:30:00"

// 解析
LocalDateTime parsed = LocalDateTime.parse("2025-07-11 14:30:00", FORMATTER);

// ⚠️ 类型要匹配
// LocalDate.parse("2025-07-11 14:30:00", FORMATTER);  // 异常!LocalDate 不含时间

// 预定义格式
DateTimeFormatter.ISO_LOCAL_DATE;       // "2025-07-11"
DateTimeFormatter.ISO_LOCAL_DATE_TIME;  // "2025-07-11T14:30:00"

对比 SimpleDateFormat 的线程不安全问题:

java
// ❌ 多线程共享 SimpleDateFormat → 数据错乱或异常
private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
// 多线程调用 sdf.parse() / sdf.format() 会出问题

// 如果必须使用旧 API,用 ThreadLocal 包装
private static final ThreadLocal<SimpleDateFormat> SDF_LOCAL =
    ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));

6.5 时区处理与夏令时

java
// 常用时区
ZoneId shanghai = ZoneId.of("Asia/Shanghai");    // UTC+8
ZoneId utc = ZoneId.of("UTC");                   // UTC
ZoneId newYork = ZoneId.of("America/New_York");  // UTC-5 / UTC-4(夏令时)

// 时区转换
ZonedDateTime shanghaiTime = ZonedDateTime.now(shanghai);
ZonedDateTime newYorkTime = shanghaiTime.withZoneSameInstant(newYork);

// ⚠️ 夏令时陷阱
// 美国东部:2025-03-09 02:00 → 03:00(春季跳过一小时)
// LocalDateTime 不感知时区,无法处理夏令时
// 跨夏令时边界计算时长应使用 Duration + Instant/ZonedDateTime

// 数据库存储建议:UTC 时间戳
Instant dbTimestamp = ZonedDateTime.now(shanghai).toInstant();

6.6 新旧 API 互转

java
// Date ↔ Instant
Date date = new Date();
Instant instant = date.toInstant();                     // Date → Instant
Date backToDate = Date.from(instant);                   // Instant → Date

// Date → LocalDateTime
LocalDateTime ldt = date.toInstant()
    .atZone(ZoneId.systemDefault())
    .toLocalDateTime();

// LocalDateTime → Date
Date date2 = Date.from(
    ldt.atZone(ZoneId.systemDefault()).toInstant()
);

// Calendar → ZonedDateTime
Calendar cal = Calendar.getInstance();
ZonedDateTime zdt = cal.toInstant().atZone(cal.getTimeZone().toZoneId());

最佳实践: 新项目全面使用 java.time,仅在与旧 API(如 JDBC java.sql.Timestamp)交互的边界做转换。


7. 最佳实践与常见坑

7.1 String 相关

#正确做法
1循环内用 + 拼接字符串循环外创建 StringBuilder,循环内 append()
2"str" == anotherStr 比较字符串使用 "str".equals(anotherStr)(常量放前面防 NPE)
3split(".") 得到空数组split("\\."). 是正则通配符)
4String.matches() 在循环中使用预编译 Pattern 复用
5忽略 substring() 的内存问题(JDK 6)JDK 7+ 已修复,substring() 会创建新数组

7.2 包装类相关

#正确做法
1Integer == Integer 比较使用 equals()Integer.compare()
2拆箱 null 导致 NPE先判空:if (integerObj != null)
3new BigDecimal(0.1)使用 new BigDecimal("0.1")BigDecimal.valueOf(0.1)
4BigDecimal.equals() 比较值使用 compareTo() == 0
5divide() 不指定舍入模式始终指定 scaleRoundingMode

7.3 日期时间相关

#正确做法
1多线程共享 SimpleDateFormat使用 DateTimeFormatter(线程安全)
2Calendar.MONTH 从 0 开始使用 LocalDate.of(2025, 1, 15)(1 = 一月)
3LocalDateTime 处理跨时区场景使用 ZonedDateTimeInstant
4数据库存本地时间存 UTC 时间戳,展示时转换

8. 面试高频问题

Q1:为什么重写 equals 必须同时重写 hashCode?

答: 因为 HashMap/HashSet 先用 hashCode() 定位桶,再用 equals() 确认键是否相同。如果两个 equals 相等的对象 hashCode 不同,它们会被分到不同的桶中,导致 HashMapput 的键 get 不到。这违反了 Object 类中 hashCode 的通用契约。

Q2:String、StringBuilder、StringBuffer 的区别?

答:

  • String:不可变,线程安全,每次修改创建新对象
  • StringBuilder:可变,非线程安全,性能高,单线程首选
  • StringBuffer:可变,线程安全(synchronized),性能低,几乎不用

循环拼接场景必须用 StringBuilder,避免 $O(n^2)$ 复杂度。

Q3:new String("abc") 创建了几个对象?

答: 最多 2 个。首先检查常量池中是否存在 "abc",不存在则在常量池创建一个;然后在堆上 new 一个 String 对象。如果常量池已有 "abc",则只创建堆上的 1 个对象。

Q4:BigDecimal 的 equals 和 compareTo 有什么区别?

答: equals() 比较值和精度(scale)new BigDecimal("2.0").equals(new BigDecimal("2.00")) 返回 false(scale 分别是 1 和 2)。compareTo() 只比较数学值,返回 0 表示相等。业务中比较大小应使用 compareTo()

Q5:为什么 SimpleDateFormat 不是线程安全的?而 DateTimeFormatter 是?

答: SimpleDateFormat 内部使用 Calendar 实例存储中间解析状态(可变状态),多线程并发调用会导致状态混乱。DateTimeFormatter 采用不可变设计,所有解析方法不依赖实例状态,天然线程安全,可声明为 static final 常量全局复用。


9. 总结

mermaid
graph LR
    A["Object"] -->|"equals+hashCode"| B["HashMap/HashSet 正确性"]
    C["String"] -->|"不可变"| D["线程安全 + 常量池复用"]
    C -->|"拼接优化"| E["StringBuilder > +"]
    F["包装类"] -->|"equals 比较"| G["避免 == 陷阱"]
    F -->|"BigDecimal"| H["精确计算 + compareTo"]
    I["java.time"] -->|"不可变"| J["线程安全"]
    I -->|"DateTimeFormatter"| K["替代 SimpleDateFormat"]
核心要点一句话记忆
equals/hashCode重写 equals 必须重写 hashCode,用 Objects.hash()
String 不可变性线程安全、hashCode 可缓存、常量池可复用
字符串拼接循环内用 StringBuilder,JDK 9+ 简单拼接自动优化
包装类比较永远用 equals(),不要用 ==
BigDecimal用 String 构造,compareTo() 比较,divide() 指定舍入
日期时间全面使用 java.timeDateTimeFormatter 线程安全可复用
存储时间数据库存 UTC 时间戳,展示层按时区转换